package top.lc951.myglide.manager;

import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkInfo;
import android.os.Build;
import android.util.Log;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;

import org.apache.http.params.CoreConnectionPNames;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import top.lc951.myglide.util.GlideSuppliers;
import top.lc951.myglide.util.Synthetic;
import top.lc951.myglide.util.Util;

final class SingletonConnectivityReceiver {
    private static volatile SingletonConnectivityReceiver instance;
    private static final String TAG = "ConnectivityMonitor";

    private final FrameworkConnectivityMonitor frameworkConnecivityMonitor;

    @GuardedBy("this")
    @Synthetic
    final Set<ConnectivityMonitor.ConnectivityListener> listeners = new HashSet();

    @GuardedBy("this")
    private boolean isRegistered;

    static SingletonConnectivityReceiver get(@NonNull Context context) {
        if (instance == null) {
            synchronized (SingletonConnectivityReceiver.class) {
                if (instance == null) {
                    instance = new SingletonConnectivityReceiver(context.getApplicationContext());
                }
            }
        }
        return instance;
    }

    @VisibleForTesting
    static void reset() {
        instance = null;
    }

    private SingletonConnectivityReceiver(final @NonNull Context context) {
        GlideSuppliers.GlideSupplier<ConnectivityManager> connectivityManager =
                GlideSuppliers.memorize(
                        new GlideSuppliers.GlideSupplier<ConnectivityManager>() {
                            @Override
                            public ConnectivityManager get() {
                                return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
                            }
                        }
                );
        ConnectivityMonitor.ConnectivityListener connectivityListener =
                new ConnectivityMonitor.ConnectivityListener() {
                    @Override
                    public void onConnectivityChanged(boolean isConnected) {
                        List<ConnectivityMonitor.ConnectivityListener> toNotify;
                        synchronized (SingletonConnectivityReceiver.this) {
                            toNotify = new ArrayList<>(listeners);
                        }
                        for (ConnectivityMonitor.ConnectivityListener listener : toNotify) {
                            listener.onConnectivityChanged(isConnected);
                        }
                    }
                };

        frameworkConnecivityMonitor =
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
                        ? new FrameworkConnectivityMonitorPostApi24(connectivityManager,connectivityListener)
                        : new FrameworkConnectivityMonitorPreApi24(
                        context, connectivityManager, connectivityListener
                );
    }

    private interface FrameworkConnectivityMonitor {
        boolean register();

        void unregister();
    }

    @RequiresApi(Build.VERSION_CODES.N)
    private static final class FrameworkConnectivityMonitorPostApi24
            implements FrameworkConnectivityMonitor {
        @Synthetic
        boolean isConnected;
        @Synthetic
        final ConnectivityMonitor.ConnectivityListener listener;
        private final GlideSuppliers.GlideSupplier<ConnectivityManager> connectivityManager;
        private final ConnectivityManager.NetworkCallback networkCallback =
                new ConnectivityManager.NetworkCallback() {
                    @Override
                    public void onAvailable(@NonNull Network network) {
                        postOnConnectivityChange(true);
                    }

                    @Override
                    public void onLost(@NonNull Network network) {
                        postOnConnectivityChange(false);
                    }

                    private void postOnConnectivityChange(final boolean newState) {
                        // We could use registerDefaultNetworkCallback with a Handler, but that's only available
                        // on API 26, instead of API 24. We can mimic the same behavior here manually by
                        // posting to the UI thread. All calls have to be posted to make sure that we retain the
                        // original order. Otherwise a call on a background thread, followed by a call on the UI
                        // thread could result in the first call running second.
                        Util.postOnUiThread(
                                new Runnable() {
                                    @Override
                                    public void run() {
                                        onConnectivityChange(newState);
                                    }
                                });
                    }

                    @Synthetic
                    void onConnectivityChange(boolean newState) {
                        // See b/201425456.
                        Util.assertMainThread();

                        boolean wasConnected = isConnected;
                        isConnected = newState;
                        if (wasConnected != newState) {
                            listener.onConnectivityChanged(newState);
                        }
                    }
                };

        FrameworkConnectivityMonitorPostApi24(
                GlideSuppliers.GlideSupplier<ConnectivityManager> connectivityManager, ConnectivityMonitor.ConnectivityListener listener) {
            this.connectivityManager = connectivityManager;
            this.listener = listener;
        }
        // Permissions are checked in the factory instead.
        @SuppressLint("MissingPermission")
        @Override
        public boolean register() {
            isConnected = connectivityManager.get().getActiveNetwork()!=null;
            try {
            connectivityManager.get().registerDefaultNetworkCallback(networkCallback);
            return true;
                // See b/201664814, b/204226444: At least TooManyRequestsException is not public and
                // doesn't extend from any subclass :/.
            } catch (RuntimeException e) {
                if (Log.isLoggable(TAG, Log.WARN)) {
                    Log.w(TAG, "Failed to register callback", e);
                }
                return false;
            }
        }

        @Override
        public void unregister() {
                connectivityManager.get().unregisterNetworkCallback(networkCallback);
        }
    }

    private static final class FrameworkConnectivityMonitorPreApi24
            implements FrameworkConnectivityMonitor {
        private final Context context;
        @Synthetic final ConnectivityMonitor.ConnectivityListener listener;
        private final GlideSuppliers.GlideSupplier<ConnectivityManager> connectivityManager;
        @Synthetic boolean isConnected;
        private final BroadcastReceiver connectivityReceiver =
                new BroadcastReceiver() {
                    @Override
                    public void onReceive(@NonNull Context context, Intent intent) {
                        boolean wasConnected = isConnected;
                        isConnected = isConnected();
                        if (wasConnected != isConnected) {
                            if (Log.isLoggable(TAG, Log.DEBUG)) {
                                Log.d(TAG, "connectivity changed, isConnected: " + isConnected);
                            }

                            listener.onConnectivityChanged(isConnected);
                        }
                    }
                };
        FrameworkConnectivityMonitorPreApi24(
                Context context,
                GlideSuppliers.GlideSupplier<ConnectivityManager> connectivityManager,
                ConnectivityMonitor.ConnectivityListener listener) {
            this.context = context.getApplicationContext();
            this.connectivityManager = connectivityManager;
            this.listener = listener;
        }

        @Override
        public boolean register() {
            // Initialize isConnected so that we notice the first time around when there's a broadcast.
            isConnected = isConnected();
            try {
                // See #1405
                context.registerReceiver(
                        connectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
                return true;
            } catch (SecurityException e) {
                // See #1417, registering the receiver can throw SecurityException.
                if (Log.isLoggable(TAG, Log.WARN)) {
                    Log.w(TAG, "Failed to register", e);
                }
                return false;
            }
        }

        @Override
        public void unregister() {
            context.unregisterReceiver(connectivityReceiver);
        }
        @SuppressWarnings("WeakerAccess")
        @Synthetic
        // Permissions are checked in the factory instead.
        @SuppressLint("MissingPermission")
        boolean isConnected() {
            NetworkInfo networkInfo;
            try {
                networkInfo = connectivityManager.get().getActiveNetworkInfo();
            } catch (RuntimeException e) {
                // #1405 shows that this throws a SecurityException.
                // b/70869360 shows that this throws NullPointerException on APIs 22, 23, and 24.
                // b/70869360 also shows that this throws RuntimeException on API 24 and 25.
                if (Log.isLoggable(TAG, Log.WARN)) {
                    Log.w(TAG, "Failed to determine connectivity status when connectivity changed", e);
                }
                // Default to true;
                return true;
            }
            return networkInfo != null && networkInfo.isConnected();
        }
    }
}

